﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Timers;
using DotNetRepository.Tools;

namespace DotNetRepository.Cache
{
    internal class CacheManager<TKey, TItem> : IDisposable
        where TKey : struct
        where TItem : class
    {
        
        private readonly IDictionary<TKey, CacheItem<TKey,TItem>> _cache = new Dictionary<TKey, CacheItem<TKey,TItem>>();

        private readonly int _maxSize;
        private readonly Timer _scavengeTimer = new Timer();

        private readonly ValueLock<TKey> _lock = new ValueLock<TKey>();

        internal CacheManager() : this(30000,0){}

        internal CacheManager(int pollFrequency, int maxSize)
        {
            _maxSize = maxSize;
            _scavengeTimer.Interval = pollFrequency;
            _scavengeTimer.Elapsed += BeginScavenging;
            _scavengeTimer.Start();
        }

        public IEnumerable<TKey> Keys
        {
            get { return _cache.Keys; }
        }

        internal void Add(TKey key, TItem item, CacheItemPriority priority, 
            ICacheItemRefreshAction<TKey,TItem> refreshAction, ICacheItemExpiration<TKey,TItem> expiration)
        {
            _lock.Lock(key);
            try
            {
                if (_cache.ContainsKey(key))
                {
                    var cacheItem = _cache[key];
                    cacheItem.Hits++;
                    cacheItem.LastAccessedTime = DateTime.Now;
                }
                else
                {
                    lock (_cache)
                    {
                        var cacheItem = new CacheItem<TKey, TItem>(item, priority, expiration, refreshAction);
                        _cache.Add(key, cacheItem);
                    }
                }
            }
            finally
            {
                _lock.UnLock(key);
            }
        }

        internal void Remove(TKey key)
        {
            _lock.Lock(key);
            try
            {
                CacheItem<TKey, TItem> cacheItem;
                if (_cache.TryGetValue(key,out cacheItem))
                {
                    lock (_cache)
                    {
                        _cache.Remove(key);
                    }
                    cacheItem.Refresh(key, cacheItem.Value, CacheItemRemovedReason.Removed);
                }
            }
            finally
            {
                _lock.UnLock(key);
            }
        }

        internal TItem GetValue(TKey key)
        {
            _lock.Lock(key);
            try
            {
                if (_cache.ContainsKey(key))
                {
                    var cacheItem = _cache[key];
                    lock (cacheItem)
                    {
                        cacheItem.Hits++;
                        cacheItem.LastAccessedTime = DateTime.Now;
                    }
                    return cacheItem.Value;
                }
                else
                {
                    return null;
                }
            }
            finally
            {
                _lock.UnLock(key);
            }
        }

        private object _scavengeLocker = new object();

        private void BeginScavenging(object obj, ElapsedEventArgs elapsedEventArgs)
        {
            lock (_scavengeLocker)
            {
                Scavenge();
            }
        }

        private void Scavenge()
        {
            var keys = _cache.Keys.ToArray();

            // Remove expired objects first
            foreach (var key in keys)
            {
                _lock.Lock(key);
                try
                {
                    CacheItem<TKey, TItem> cacheItem;
                    if (_cache.TryGetValue(key, out cacheItem))
                    {
                        if (cacheItem.Expiration.HasExpired())
                        {
                            lock (_cache)
                            {
                                _cache.Remove(key);
                            }
                            cacheItem.Refresh(key, cacheItem.Value, CacheItemRemovedReason.Expired);
                        }
                    }
                }
                finally
                {
                    _lock.UnLock(key);
                }
            }

            // Now begin scavenging if needed
            if (_maxSize > 0 && _cache.Count > _maxSize)
            {
                var removalCount = _cache.Count - _maxSize;

                var keysToRemove = _cache
                    .OrderBy(kvp => kvp.Value.Priority)
                    .Take(removalCount)
                    .Where(kvp => kvp.Value.Priority != CacheItemPriority.NotRemovable)
                    .Select(kvp => kvp.Key)
                    .ToArray();

                foreach (var key in keysToRemove)
                {
                    _lock.Lock(key);
                    try
                    {
                        CacheItem<TKey, TItem> cacheItem;
                        if (_cache.TryGetValue(key, out cacheItem))
                        {
                            lock (_cache)
                            {
                                _cache.Remove(key);
                            }
                            cacheItem.Refresh(key, cacheItem.Value, CacheItemRemovedReason.Scavenged);
                        }
                    }
                    finally
                    {
                        _lock.UnLock(key);
                    }
                }
            }
        }

        internal bool Contains(TKey key)
        {
            return _cache.ContainsKey(key);
        }

        public void Dispose()
        {
            GC.SuppressFinalize(this);

            if (_cache != null)
                _cache.Clear();

            if (_scavengeTimer != null)
            {
                if (_scavengeTimer.Enabled)
                    _scavengeTimer.Stop();

                _scavengeTimer.Dispose();
            }
        }

    }

    

}
